package it.tukano.collections;

import it.tukano.fun.Cast;
import it.tukano.fun.Function1;
import it.tukano.fun.Function2;
import it.tukano.fun.Tuple2;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;

/**
 * Collections utilities
 * @author pgi
 */
public class CollectionsExt {

    /**
     * Instance initializer
     */
    CollectionsExt() {
    }

    /**
     * Transforms an iterable of tuple2 into a map
     * @param <A> the type of the key
     * @param <B> the type of the value
     * @param values the iterable of tuples
     * @return the map
     */
    public static <A, B> MapExt<A, B> toMap(Iterable<Tuple2<A, B>> values) {
        MapExt<A, B> map = MapExt.newInstance(new HashMap<A, B>());
        for (Tuple2<A, B> tuple2 : values) {
            map.put(tuple2.getA(), tuple2.getB());
        }
        return map;
    }

    /**
     * Checks if an array contains an item
     * @param <T> the type of the element of the array
     * @param array the array to scan
     * @param value the value to find
     * @return true if value is a component of array
     */
    public static <T> boolean contains(T[] array, T value) {
        for (int i = 0; i < array.length; i++) {
            T t = array[i];
            if(t != null && t.equals(value)) return true;
        }
        return false;
    }

    /**
     * Returns the elements contained in a that are not contained in b.
     * @param <T> the type of the elements
     * @param a the first set of elements
     * @param b the second set of elements
     * @return the set of elements found in a that are not contained in b
     */
    public static <T> ListExt<T> difference(Iterable<? extends T> a, Collection<T> b) {
        ListExt<T> diff = ListExt.<T>newLinkedListExt();
        for (T object : a) {
            if(!b.contains(object)) diff.add(object);
        }
        return diff;
    }

    /**
     * Converts the elements of a set in a set of elements of a different type
     * @param <IN> the input type
     * @param <OUT> the output type
     * @param source the elements to convert
     * @param outType the destination type
     * @return a set of the elements of source compatible with the output type
     */
    public static <IN, OUT> ListExt<OUT> cast(Iterable<? extends IN> source, Class<OUT> outType) {
        Cast<IN, OUT> cast = new Cast<IN, OUT>(outType);
        ListExt<OUT> list = ListExt.<OUT>newLinkedListExt();
        for (IN a : source) {
            OUT conversion = cast.apply(a);
            if(conversion != null) list.add(conversion);
        }
        return list;
    }

    /**
     * Generate a new set of elements applying a function to each element of the source set
     * @param <IN> the input type
     * @param <OUT> the output type
     * @param source the source set
     * @param fun the mapping function
     * @return a new set of elements
     */
    public static <IN, OUT> ListExt<OUT> collect(Iterable<? extends IN> source, Function1<IN, Iterable<OUT>> fun) {
        ListExt<OUT> result = ListExt.<OUT>newLinkedListExt();
        for(IN in : source) {
            Iterable<OUT> out = fun.apply(in);
            for (OUT object : out) {
                result.add(object);
            }
        }
        return result;
    }

    /**
     * Transforms a set of elements in a set of elements of different type and size
     * @param <IN> the input type
     * @param <OUT> the output type
     * @param source the source set
     * @param fun the transform function
     * @return the transformed set
     */
    public static <IN, OUT> ListExt<OUT> simpleCollect(Iterable<? extends IN> source, Function1<IN, OUT> fun) {
        ListExt<OUT> result = ListExt.<OUT>newLinkedListExt();
        for(IN in : source) {
            OUT out = fun.apply(in);
            if(out != null) result.add(out);
        }
        return result;
    }

    /**
     * Transforms a set of elements passing the iteration index to the transformation function
     * @param <IN> the input type
     * @param <OUT> the output type
     * @param source the set to transform
     * @param fun the transform function
     * @return a new list of values
     */
    public static <IN, OUT> ListExt<OUT> simpleIndexedCollect(Iterable<? extends IN> source, Function2<IN, Integer, OUT> fun) {
        ListExt<OUT> result = ListExt.<OUT>newLinkedListExt();
        int index = 0;
        for(IN in : source) {
            OUT out = fun.apply(in, index);
            index++;
            if(out != null) result.add(out);
        }
        return result;
    }

    /**
     * Finds an element into a list given a matching type and returns it. If no
     * such element exists, add a new element to the list and returns it
     * @param <T> the type of the list
     * @param <K> the type of the element to find
     * @param typeToFind the class type of the element to find
     * @param list the list to scan or modify
     * @param instance the instance to add if no value is found
     * @return the found element or instance
     */
    public static <T, K extends T> K findOrAdd(Class<K> typeToFind, List<T> list, K instance) {
        for (Object object : list) {
            if(typeToFind.isAssignableFrom(object.getClass())) return typeToFind.cast(object);
        }
        list.add(instance);
        return instance;
    }
}
